Skip to content

feat(devx): hold this repo's own shell to a bash 3.2 floor - #8016

Merged
baozhoutao merged 1 commit into
mainfrom
claude/issue-7692-bash32-floor-gate
Sep 6, 2026
Merged

feat(devx): hold this repo's own shell to a bash 3.2 floor#8016
baozhoutao merged 1 commit into
mainfrom
claude/issue-7692-bash32-floor-gate

Conversation

@claude

@claude claude Bot commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

Fixes #7692

macOS ships bash 3.2.57 and no bash 4+, for licensing reasons. CI runs bash 5, where every bash-4-only construct works. So a mapfile, a declare -A or an unguarded $EPOCHSECONDS in a hand-run script is invisible to a normal green run: the defect and its repair both read as green, and the only reader who ever sees the failure is a contributor on a Mac at the moment they most need the script to work.

This repository held its own shell to nothing. The sibling repo has gated every tracked shell file at bash 3.2 for some time; here, four shell files this repository wrote itself were held to no floor at all — and two of them (e2e/live/ci/start-backend.sh, e2e/live/ci/stop-backend.sh) are not even under the sibling gate's scan roots.

What landed

file change
scripts/check-bash32-floor.mjs new — ported from objectstack scripts/check-bash32-floor.mjs at 6136293, with this repository's scan roots
scripts/__tests__/bash32-floor-wiring.test.ts new — the wiring pin, modelled on entry-guard-wiring.test.ts
package.json one script: check:bash32-floor
.github/workflows/lint.yml one step in the existing lint job, beside the other pre-install repo gates

No new workflow. No timeout change. No .sh file edited. No changeset owed (check-changeset-presence: "0 of them published source of a package the release covers … no changeset is owed").

The one substantive change: e2e/**

Upstream declares scripts/**, .claude/hooks/**, .githooks/**. This port declares scripts/**, .claude/hooks/**, e2e/**.

This repository has no .githooks/ at all, and two of the four shell files it wrote itself live under e2e/. Copying upstream's roots verbatim would have walked past half the population this card is about while printing a confident green line — which is the exact failure the gate exists to refuse. The root is pinned in both directions in bash32-floor-wiring.test.ts: that it is declared, and that the walk actually honours it.

The measurement the card asked for

The card grepped 10 of the 19 constructs by hand and was explicit that the result must not be quoted as a clean bill. Here is the full-population verdict, which is the gate's own output on this tree at 17c1b86:

✓ check-bash32-floor: 12 tracked shell file(s) under scripts/**, .claude/hooks/**, e2e/** name no bash 4+ construct outside a comment, a guarded ${VAR:-} read, or a non-command position.
  census: 12 by .sh extension, 0 by shebang alone; 19 constructs checked, floor bash 3.2.

19 of 19, all 12 files, 0 findings. So this is a ratchet, not a repair — and the other 9 constructs are now a measurement rather than an assumption.

Evidence that the instrument can fail

A gate whose pattern matches nothing reports a clean tree forever, so each leg below was driven on disk (mutation proven by anchored counts plus git hash-object against the HEAD blob, restored under a trap with absolute paths, restore proven by an empty git diff HEAD).

Reach of the e2e/** root — the whole point of the card. A declare -A backend_env planted in e2e/live/ci/start-backend.sh:

control exit=0
blob before=35ec0ed394a2d953f4ae473c6645105d1652f895
blob after =7ef7b13dda8fbed9e0c1f87e3a400bb23293434e
mutated exit=1
✗ check-bash32-floor: 1 bash-4+ construct(s) in shell this repo ships.
  e2e/live/ci/start-backend.sh:114
      declare -A backend_env
      declare -A / local -A / typeset -A / readonly -A — bash 4.0; the floor is 3.2 (macOS ships 3.2.57).
      On 3.2: `declare: -A: invalid option` — the array is never created and every later read is empty
restored blob=35ec0ed394a2d953f4ae473c6645105d1652f895 ; git diff HEAD: empty
restored exit=0

The self-test really fails. Narrowing the roots back to upstream's reds the wiring pin (3 of 9 cases); gutting the assoc-array pattern reds --self-test (4 of 155 cases). Both restored to the HEAD bytes.

What this port changed about upstream's text, and why

Every claim in the ported header that was a measurement about the sibling tree is either re-measured here or labelled as upstream's. The two that changed behaviour:

  1. The real-tree byShebang > 0 leg is gone. Upstream measures 2 shebang-only scripts and calls the discovery half load-bearing. Measured here: all 12 shell files carry a .sh name, so the shebang census is 0 and upstream's leg would have been red on day one. ⛔ The wrong repair would be to drop shebang discovery — it is the half that catches the first extension-less script anyone adds. Instead its mechanism moved to where it can actually be exercised (the end-to-end fixture now plants a shebang-only script under a walked root), and the real-tree battery got two legs that are true here: the census partitions the population, and every member was really read.
  2. The clean-tree census leg asserts numbers, not the word "shebang". Upstream matches /shebang/ on the green line — a word that line prints unconditionally, so the leg passes on a discovery path that found nothing.

Both self-test battery floors grew (5→6, 2→3); neither was lowered. Case count 153 → 155.

Why this is not registered in scripts/upstream-port-pin.json

The ledger would be valuable here — this file's whole subject is that a denylist rots, and upstream actively sweeps the table. It is still not registered, for a structural reason rather than a judgement:

The pin carries one global upstream.ref (bf10deb today), not one per file, and --resync rewrites it for every entry (pin.upstream.ref = ref;). This port was taken at 6136293. Measured over the API, the upstream file exists at both refs — 55,415 bytes at bf10deb against 62,481 at 6136293 — and the difference is the -v unary widening plus the 4.0 operator sweep. So registering would mean either deliberately shipping the older, weaker construct table, or bumping the global ref and re-syncing three unrelated ported files. ⛔ The third option — register 6136293's digest under a global ref that reads bf10deb — is the one that must not be taken: the digest would verify while the provenance line was false.

This limitation is already owned by objectui#7953, measured from the other direction (two ported hooks that do not exist upstream at bf10deb at all). Not re-filed. The consequence is written into the gate's header so it is inherited rather than rediscovered: this file has no drift gate.

Falsified dispatch assumptions

  • The sibling's self-test is not a separate file. It is inline (--self-test, 153 cases). This repository's idiom for exactly that shape (check-entry-guard, check-upstream-port-parity) is inline battery + a *-wiring.test.ts pin, so that is what shipped.
  • ci-cd-pipeline-doc.test.ts does not red, and no doc row is owed. Its job-table and first-party-command pins read ci.yml only (const ciWorkflow = fs.readFileSync(path.join(workflowDir, 'ci.yml'), 'utf8')); lint.yml is covered only by the workflow-inventory pin, which wants a heading and already has one. Run targeted: 40 passed.
  • No timeout change. timeout-minutes: 25 is derived from this job's own distribution (max 7m14s). Both invocations of this gate together run in well under two seconds.

⚠️ Found while measuring that: ci-cd-pipeline.md's Lint section names 1 of the 4 scripts/check-*.mjs gates that job runs — check-entry-guard, check-upstream-port-parity and check-cross-repo-closer-outcome were already absent before this PR, and this step would be a fourth. Deliberately not patched here: updating the prose alone repeats the drift, and a partially-refreshed list reads as curated. Filed unassigned as objectui#8015.

Gates run (all at 17c1b86, exit codes captured by redirect-then-capture)

command exit verdict
pnpm check:bash32-floor 0 12 files, 19 constructs, 0 findings
node scripts/check-bash32-floor.mjs --self-test 0 155 cases pass
pnpm exec vitest run scripts/__tests__/ 0 110 files, 3315 tests passed
pnpm type-check:scripts 0 clean
pnpm lint:root (whole, no narrowing) 0 32 problems, 0 errors
pnpm check:entry-guard 0 70 files, no guard outside the baseline
pnpm check:upstream-port-parity 0 3 ported files match modulo declared divergences
pnpm check:control-bytes 0 6445 tracked text files
node scripts/check-pre-install-import-graph.mjs 0 23 gates, every non-relative leaf a node builtin
node scripts/check-changeset-presence.mjs 0 no changeset owed
node scripts/check-governed-queue-guard.mjs --test … 0 NOT GOVERNED (4 paths)

Plus a grep -naP control-byte self-scan of all four changed paths: clean.

Live E2E (informational) is red on every branch today for an upstream reason and is not this change's.


Generated by Claude Code

macOS ships bash 3.2.57 and no bash 4+; CI runs bash 5. So a `mapfile`, a
`declare -A` or an unguarded `$EPOCHSECONDS` in a hand-run script is green in
CI, green locally on Linux, and dies only on the host the floor exists for.
This repository held its shell to nothing: measured on `origin/main` 28cfff4,
0 tracked files named bash32/bash-32 against a control of 47 existing
`scripts/check-*.mjs` gates.

Ports `scripts/check-bash32-floor.mjs` from objectstack at 6136293 with THIS
repository's scan roots. `e2e/**` replaces upstream's `.githooks/**` and is the
substantive change: two of the four shell files this repository wrote itself
live in `e2e/live/ci/`, so upstream's roots would have walked past half the
population while printing a green line.

First full-population run on this tree: 12 shell files, 19 constructs,
0 findings — a ratchet, not a repair. The card that filed it had grepped 10 of
the 19 constructs by hand; the gate makes the other 9 a measurement.

Every claim in the ported header that was a measurement about the SIBLING tree
is re-measured here or labelled as upstream's. Two self-test legs differ from
upstream and both grew rather than shrank: the clean-tree fixture now asserts
both census halves by number instead of matching the word "shebang" (which the
green line prints unconditionally), and upstream's `byShebang > 0` real-tree leg
is replaced by a census-partition leg plus an every-member-was-read leg, because
all 12 shell files here carry a `.sh` name.

Deliberately NOT registered in `scripts/upstream-port-pin.json`: that ledger
carries one global `upstream.ref` (bf10deb) which `--resync` rewrites for every
entry, so registering would mean either shipping bf10deb's older, weaker
construct table or re-syncing three unrelated ported files.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FhBNJcLRZLe8M87VcUgpKr

Copy link
Copy Markdown
Contributor

Standing-down note — Live E2E (informational) is red on this head (17c1b86) as it is on main and on every branch today: the upstream base-red anchored on #7990 (objectstack#16186). It is continue-on-error by design, never blocks the queue, and touches nothing this diff changes (a new scripts/ gate, its wiring pin, one package.json script, one lint.yml step). Not this PR's to fix; no re-run requested. Lint and Type Check are green on this head at 09:58Z; the four test shards are still running. ACCEPTed on the card (5558467305); flip and arming follow once the shards conclude.


Generated by Claude Code

@baozhoutao
baozhoutao marked this pull request as ready for review September 6, 2026 10:06
@baozhoutao
baozhoutao added this pull request to the merge queue Sep 6, 2026
Merged via the queue into main with commit 4922b83 Sep 6, 2026
30 of 31 checks passed
@baozhoutao
baozhoutao deleted the claude/issue-7692-bash32-floor-gate branch September 6, 2026 10:21
baozhoutao pushed a commit that referenced this pull request Sep 6, 2026
Batch 19 (PR #8017, packages/collaboration/README.md) and objectui#7692
(PR #8016) landed while this batch was measuring. The two UNGATED_DOCS
removals are different lines of the same object literal, so the merge is
clean; every gate, test, probe and reading is re-run on the merged head.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FhBNJcLRZLe8M87VcUgpKr
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants